/*
 * Copyright (C) 2011, EADS France
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 */
package xowl.interpreter.backends.rdf;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import xowl.interpreter.backends.rete.RETERule;
import xowl.interpreter.backends.rete.Condition;
import xowl.interpreter.backends.rete.Filter;
import xowl.interpreter.backends.rete.RETENetwork;
import xowl.interpreter.backends.rete.Token;
import xowl.interpreter.backends.rete.TokenActivable;
import xowl.interpreter.backends.rete.TripleFact;

/**
 *
 * @author Laurent WOUTERS
 */
public class RDFRuleEngine implements RDFChangeListener {
    protected static class Request {
        public xowl.lang.owl2.Ontology target;
        public xowl.lang.owl2.Ontology meta;
        public RDFRule rule;
        public Map<XOWLVariableNode, RDFNode> bindings;
        public Map<XOWLVariableNode, RDFNode> creations;
    }
    
    protected RDFRepository repository;
    protected RDFChangeListener output;
    protected Map<xowl.lang.rules.Rule, RETERule> rules;
    protected RDFRuleTranslator translator;
    protected RETENetwork network;
    protected EntityCreator creator;
    protected Map<Token, Request> requestsFire;
    protected List<Token> requestsUnfire;
    protected Map<Token, RDFChangeset> executed;
    protected boolean executing;
    protected boolean hasChanged;
    protected RDFChangeset outputBuffer;
    
    public RDFRuleEngine(RDFRepository repo, RDFChangeListener output) {
        this.repository = repo;
        this.output = output;
        this.rules = new HashMap<xowl.lang.rules.Rule, RETERule>();
        this.translator = new RDFRuleTranslator(repository, repository.getGraph());
        this.network = new xowl.interpreter.backends.rete.RETENetwork();
        this.creator = new DefaultEntityCreator(repo);
        this.requestsFire = new HashMap<Token, Request>();
        this.requestsUnfire = new ArrayList<Token>();
        this.executed = new HashMap<Token, RDFChangeset>();
        this.executing = false;
        this.hasChanged = false;
    }
    
    public void addRules(xowl.interpreter.impl.AggregatedEvaluator evaluator, xowl.lang.owl2.Ontology input, xowl.lang.owl2.Ontology target, xowl.lang.owl2.Ontology meta, xowl.interpreter.RuleSet ruleset) {
        List<RETERule> reteRules = new ArrayList<RETERule>();
        for (xowl.lang.rules.Rule rule : ruleset.getRules()) {
            try {
                RETERule reteRule = buildRETERule(evaluator, input, target, meta, rule);
                this.rules.put(rule, reteRule);
                reteRules.add(reteRule);
            } catch (xowl.interpreter.XOWLException ex) {
                repository.getLog().error(RDFRepository.name, ex.getHeadline(), ex);
            }
        }
        network.addRules(reteRules);
        onChangeInjected(evaluator);
    }
    public void removeRules(xowl.interpreter.RuleSet ruleset) {
        for (xowl.lang.rules.Rule rule : ruleset.getRules()) {
            network.removeRule(rules.get(rule));
            rules.remove(rule);
        }
    }
    public Collection<xowl.lang.rules.Rule> getRules() { return rules.keySet(); }
    
    private RETERule buildRETERule(xowl.interpreter.impl.AggregatedEvaluator evaluator, final xowl.lang.owl2.Ontology input, final xowl.lang.owl2.Ontology target, final xowl.lang.owl2.Ontology meta, xowl.lang.rules.Rule rule) throws xowl.interpreter.XOWLException {
        final RDFRule rdfRule = translator.translate(evaluator, rule);
        Filter filterInput = null;
        Filter filterMeta = null;
        if (input != null) {
            filterInput = new Filter() {
                @Override public boolean pass(TripleFact fact) { return (fact.getOntology() == input); }
            };
        }
        if (meta != null) {
            filterMeta = new Filter() {
                @Override public boolean pass(TripleFact fact) { return (fact.getOntology() == meta); }
            };
        }
        TokenActivable ruleOutput = new TokenActivable() {
            @Override public void activateToken(Token token) {
                Request request = new Request();
                request.target = target;
                request.meta = meta;
                request.rule = rdfRule;
                requestsFire.put(token, request);
            }
            @Override public void deactivateToken(Token token) {
                if (requestsFire.containsKey(token)) {
                    requestsFire.remove(token); // cancel firing request
                } else if (executed.containsKey(token)) {
                    requestsUnfire.add(token); // request unfiring
                }
            }
            @Override public void activateTokens(Collection<Token> tokens) {
                for (Token token : tokens)
                    activateToken(token);
            }
            @Override public void deactivateTokens(Collection<Token> tokens) {
                for (Token token : tokens)
                    deactivateToken(token);
            }
        };
        RETERule reteRule = new RETERule(ruleOutput);
        for (RDFTriple triple : rdfRule.getPositiveAntecedentAxioms())
            reteRule.getPositives().add(new Condition(filterInput, triple));
        for (RDFTriple triple : rdfRule.getPositiveAntecedentMeta())
            reteRule.getPositives().add(new Condition(filterMeta, triple));
        for (List<RDFTriple> list : rdfRule.getNegativeAntecedentAxioms()) {
            List<Condition> conds = new ArrayList<Condition>(list.size());
            for (RDFTriple triple : list)
                conds.add(new Condition(filterInput, triple));
            reteRule.getNegatives().add(conds);
        }
        for (List<RDFTriple> list : rdfRule.getNegativeAntecedentMeta()) {
            List<Condition> conds = new ArrayList<Condition>(list.size());
            for (RDFTriple triple : list)
                conds.add(new Condition(filterMeta, triple));
            reteRule.getNegatives().add(conds);
        }
        return reteRule;
    }
    
    @Override public void onChange(xowl.interpreter.impl.AggregatedEvaluator evaluator, RDFChange change) {
        network.inject(change);
        onChangeInjected(evaluator);
    }
    @Override public void onChanges(xowl.interpreter.impl.AggregatedEvaluator evaluator, RDFChangeset changeset) {
        network.inject(changeset);
        onChangeInjected(evaluator);
    }
    
    protected void onChangeInjected(xowl.interpreter.impl.AggregatedEvaluator evaluator) {
        if (executing)
            return;
        executing = true;
        try {
            hasChanged = true;
            while (hasChanged) {
                hasChanged = false;
                outputBuffer = new RDFChangeset();
                unfire();
                fire(evaluator);
                output.onChanges(evaluator, outputBuffer);
                outputBuffer = null;
            }
        } catch (java.lang.Exception ex) {
            repository.getLog().fatal(RDFRepository.name, "Error while firing or unfiring a RETE rule.", ex);
        }
        executing = false;
    }
    
    private void fire(xowl.interpreter.impl.AggregatedEvaluator evaluator) {
        Map<Token, Request> buffer = new HashMap<Token, Request>(requestsFire);
        requestsFire.clear();
        for (Entry<Token, Request> entry : buffer.entrySet())
            fire(evaluator, entry.getKey(), entry.getValue());
    }
    private void fire(xowl.interpreter.impl.AggregatedEvaluator evaluator, Token token, Request request) {
        if (!canFire(evaluator, token, request)) {
            requestsFire.put(token, request);
        } else {
            repository.getLog().debug(RDFRepository.name, "Firing " + request.rule.getIRI());
            RDFChangeset changeset = buildChangeset(evaluator, token, request);
            executed.put(token, changeset);
            for (RDFChange change : changeset.getRDFChanges())
                outputBuffer.add(change);
            hasChanged = true;
        }
    }
    private void unfire() {
        List<Token> buffer = new ArrayList<Token>(requestsUnfire);
        requestsUnfire.clear();
        for (Token token : buffer)
            unfire(token);
    }
    private void unfire(Token token) {
        repository.getLog().debug(RDFRepository.name, "Unfiring");
        RDFChangeset changeset = executed.get(token).getNegative();
        executed.remove(token);
        for (RDFChange change : changeset.getRDFChanges())
            outputBuffer.add(change);
        hasChanged = true;
    }
    
    private boolean canFire(xowl.interpreter.impl.AggregatedEvaluator evaluator, Token token, Request request) {
        if (request.rule.getGuards().isEmpty())
            return true;
        QueryVariableEvaluator qve = new QueryVariableEvaluator(repository, token.getBindings());
        evaluator.addPart(qve);
        for (xowl.lang.owl2.LiteralExpression guard : request.rule.getGuards()) {
            try {
                if (!xowl.utils.Values.isTrue(evaluator.evalLiteral(evaluator, guard))) {
                    evaluator.removePart(qve);
                    return false;
                }
            } catch (xowl.interpreter.XOWLException ex) {
                evaluator.removePart(qve);
                return false;
            }
        }
        evaluator.removePart(qve);
        return true;
    }
    
    private RDFChangeset buildChangeset(xowl.interpreter.impl.AggregatedEvaluator evaluator, Token token, Request request) {
        request.bindings = token.getBindings();
        request.creations = new HashMap<XOWLVariableNode, RDFNode>();
        QueryVariableEvaluator qve = new QueryVariableEvaluator(repository, token.getBindings());
        evaluator.addPart(qve);
        RDFChangeset changeset = new RDFChangeset();
        for (RDFTriple triple : request.rule.getPositiveConsequentAxioms())
            changeset.add(new RDFChange(processTriple(evaluator, triple, request), request.target, true));
        for (RDFTriple triple : request.rule.getPositiveConsequentMeta())
            changeset.add(new RDFChange(processTriple(evaluator, triple, request), request.meta, true));
        for (List<RDFTriple> group : request.rule.getNegativeConsequentAxioms()) {
            for (RDFTriple triple : group)
                changeset.add(new RDFChange(processTriple(evaluator, triple, request), request.target, false));
        }
        for (List<RDFTriple> group : request.rule.getNegativeConsequentMeta()) {
            for (RDFTriple triple : group)
                changeset.add(new RDFChange(processTriple(evaluator, triple, request), request.meta, false));
        }
        evaluator.removePart(qve);
        return changeset;
    }
    
    private RDFTriple processTriple(xowl.interpreter.impl.AggregatedEvaluator evaluator, RDFTriple triple, Request request) {
        RDFSubjectNode subject = (RDFSubjectNode)processNode(evaluator, triple.getSubject(), request);
        RDFProperty property = (RDFProperty)processNode(evaluator, triple.getProperty(), request);
        RDFNode object = processNode(evaluator, triple.getObject(), request);
        return new RDFTriple(subject, property, object);
    }
    private RDFNode processNode(xowl.interpreter.impl.AggregatedEvaluator evaluator, RDFNode node, Request request) {
        if (node.getNodeType() == RDFNodeType.Variable) {
            XOWLVariableNode varNode = (XOWLVariableNode)node;
            if (request.rule.isAntecedentVar(varNode)) {
                return request.bindings.get(varNode);
            } else {
                if (request.creations.containsKey(varNode))
                    return request.creations.get(varNode);
                xowl.lang.runtime.Entity entity = repository.resolveEntity(creator.createIRI(request.target));
                RDFNode iriNode = repository.getGraph().getEntityNode(entity.getHasIRI());
                request.creations.put(varNode, iriNode);
                return iriNode;
            }
        } else if (node.getNodeType() == RDFNodeType.Dynamic) {
            XOWLDynamicNode dynNode = (XOWLDynamicNode)node;
            try {
                xowl.lang.runtime.Value value = evaluator.evaluate(evaluator, dynNode.getDynamicExpression());
                return repository.getRDFNode(value);
            } catch (xowl.interpreter.XOWLException ex) {
                repository.getLog().fatal(RDFRepository.name, "Error while processing node in consequent triple of RETE rule.", ex);
                return null;
            }
        }
        return node;
    }
}
